/*
 * Copyright (C) 2011 Christopher Probst
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * 
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *  
 * * Neither the name of the 'FoxNet Codec' nor the names of its
 *   contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.foxnet.codec.impl2;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;

import org.objenesis.Objenesis;
import org.objenesis.ObjenesisStd;

import com.foxnet.codec.Codec;
import com.foxnet.codec.CodecRegistry;
import com.foxnet.codec.IStream;

/**
 * @author Christopher Probst
 */
final class DefaultInput extends AbstractBufferedResource implements IStream {

    /*
     * Used to instantiate new objects without a default constructor!
     */
    private static final Objenesis OBJENESIS = new ObjenesisStd();

    private InputStream verifyInputStream() {
	if (inputStream == null) {
	    throw new IllegalStateException("The input stream has not "
		    + "be set yet.");
	}
	return inputStream;
    }

    /**
     * This method guarantees that minLen-bytes are read from the stream and
     * blocks if necessary. But this method can read more than minLen. In this
     * case the remaining bytes are stored in the internal buffer. If the
     * underlying stream reaches its EOF before completing an EOFException will
     * be thrown.
     * 
     * @return Returns an integer bigger or equal to minLen.
     */
    private int readFromStream(byte[] buffer, int offset, int len, int minLen)
	    throws IOException {

	// Verify
	InputStream tmp = verifyInputStream();

	// Default checks
	if (buffer == null) {
	    throw new NullPointerException("buffer");
	} else if (offset < 0 || len < 0 || len > buffer.length - offset) {
	    throw new IndexOutOfBoundsException("buffer");
	} else if (len == 0) {
	    return 0;
	}

	if (minLen > len) {
	    throw new IllegalArgumentException("minLen cannot be "
		    + "greater than len.");
	}

	// Helper vars
	int read;
	int total = 0;

	while (total < minLen) {

	    // Read from input stream!
	    read = tmp.read(buffer, offset + total, len - total);

	    // Check for read error
	    if (read == -1) {

		// If total < minLen -> Bad!
		if (total < minLen) {
		    throw new EOFException("The stream has reached EOF "
			    + "before completing the read request.");
		}

		// End of stream reached
		return total;
	    }

	    // Increase!
	    total += read;
	}

	return total;
    }

    // Used to read the data
    private InputStream inputStream;
    // Used to store the cached objects
    private List<Object> lazyCachedObjects;
    // The initial value
    private final int cachedObjectsListSize;
    // Used to use java serialization if needed!
    private ObjectInputStream lazyObjectInputStream;

    private String readString() throws IOException {
	// Read the string length
	int length = readInt();

	// Create char array
	char[] arr = new char[length];

	// Simply read the string!
	for (int i = 0, c; i < length; i++) {
	    c = readUnsignedByte();

	    switch (c >> 4) {
	    case 0:
	    case 1:
	    case 2:
	    case 3:
	    case 4:
	    case 5:
	    case 6:
	    case 7:
		arr[i] = (char) c;
		continue;
	    case 12:
	    case 13:
		arr[i] = (char) ((c & 0x1F) << 6 | readByte() & 0x3F);
		continue;
	    case 14:
		arr[i] =
			(char) ((c & 0x0F) << 12 | (readByte() & 0x3F) << 6 | (readByte() & 0x3F));
		continue;
	    }
	}

	return new String(arr);
    }

    private byte[] readBytes() throws IOException {
	// Read the array length
	int length = readInt();

	// Create array
	byte[] arr = new byte[length];

	// Read array completely!
	read(arr);

	return arr;
    }

    private boolean[] readBooleans() throws IOException {
	// Read the array length
	int length = readInt();

	// Create array
	boolean[] arr = new boolean[length];

	for (int i = 0; i < length; i++) {
	    arr[i] = readBoolean();
	}

	return arr;
    }

    private char[] readChars() throws IOException {
	// Read the array length
	int length = readInt();

	// Create array
	char[] arr = new char[length];

	for (int i = 0; i < length; i++) {
	    arr[i] = readChar();
	}

	return arr;
    }

    private short[] readShorts() throws IOException {
	// Read the array length
	int length = readInt();

	// Create array
	short[] arr = new short[length];

	for (int i = 0; i < length; i++) {
	    arr[i] = readShort();
	}

	return arr;
    }

    private int[] readInts() throws IOException {
	// Read the array length
	int length = readInt();

	// Create array
	int[] arr = new int[length];

	for (int i = 0; i < length; i++) {
	    arr[i] = readInt();
	}

	return arr;
    }

    private long[] readLongs() throws IOException {
	// Read the array length
	int length = readInt();

	// Create array
	long[] arr = new long[length];

	for (int i = 0; i < length; i++) {
	    arr[i] = readLong();
	}
	return arr;
    }

    private float[] readFloats() throws IOException {
	// Read the array length
	int length = readInt();

	// Create array
	float[] arr = new float[length];

	for (int i = 0; i < length; i++) {
	    arr[i] = readFloat();
	}

	return arr;
    }

    private double[] readDoubles() throws IOException {
	// Read the array length
	int length = readInt();

	// Create array
	double[] arr = new double[length];

	for (int i = 0; i < length; i++) {
	    arr[i] = readDouble();
	}

	return arr;
    }

    private Object readPrimitiveArray(boolean cached) throws Exception {
	// Read type!
	int primitiveTypeClass = readUnsignedByte();

	switch (primitiveTypeClass) {
	case Protocol.TC_BYTE:
	    return readBytes();
	case Protocol.TC_BOOLEAN:
	    return readBooleans();
	case Protocol.TC_CHAR:
	    return readChars();
	case Protocol.TC_SHORT:
	    return readShorts();
	case Protocol.TC_INT:
	    return readInts();
	case Protocol.TC_LONG:
	    return readLongs();
	case Protocol.TC_FLOAT:
	    return readFloats();
	case Protocol.TC_DOUBLE:
	    return readDoubles();
	default:
	    throw new IllegalStateException("Unknown primitive type class: "
		    + primitiveTypeClass);
	}
    }

    private Object readArray(boolean cached) throws Exception {

	// Create dim array
	int[] dims = new int[readUnsignedByte()];

	// Get dims
	int length = dims[0] = readInt();

	// Convert to real array for fast access
	Object[] realArray =
		(Object[]) Array.newInstance(
			Class.forName((String) readObject()), dims);

	// Put into cache!
	assign(realArray, cached);

	// For all items
	for (int i = 0; i < length; i++) {
	    // Read object
	    realArray[i] = readObject();
	}

	return realArray;
    }

    @SuppressWarnings("unchecked")
    private Object readEnum(boolean cached) throws Exception {
	// Read string
	String enumClassName = readObject(String.class);

	// Read enum ordinal!
	int ordinal = readInt();

	// Get enum class
	Class<? extends Enum<?>> enumClass =
		(Class<? extends Enum<?>>) Class.forName(enumClassName);

	// Get enum!
	Enum<?> newEnum = enumClass.getEnumConstants()[ordinal];

	return assign(newEnum, cached);
    }

    private Object readClass(boolean cached) throws Exception {
	// Read string and get class
	return assign(Class.forName(readObject(String.class)), cached);
    }

    private Object readEmptyString() throws IOException {
	return "";
    }

    private Object readString(boolean cached) throws IOException {
	return assign(readString(), cached);
    }

    private Object readSerializableObject(boolean cached) throws Exception {

	if (lazyObjectInputStream == null) {
	    lazyObjectInputStream = new ObjectInputStream(new InputStream() {

		@Override
		public int read() throws IOException {
		    return readUnsignedByte();
		}

		/*
		 * (non-Javadoc)
		 * @see java.io.InputStream#read(byte[], int, int)
		 */
		@Override
		public int read(byte[] b, int off, int len) throws IOException {
		    // Call the default operation
		    DefaultInput.this.read(b, off, len);

		    return len;
		}
	    });
	}

	// Read object
	return assign(lazyObjectInputStream.readUnshared(), cached);
    }

    @SuppressWarnings("unchecked")
    private <T> T readOrdinaryObject(boolean cached) throws Exception {

	// Get class
	Class<T> objectClass = (Class<T>) Class.forName((String) readObject());

	// Create instance and check cache!
	T object = assign((T) OBJENESIS.newInstance(objectClass), cached);

	// Get hierarchy codecs
	Codec<?>[] codecs = getHierarchyCodecs(object.getClass());

	// Whops, no codecs!
	if (codecs.length == 0) {
	    throw new IOException("There are no bound codecs for " + object);
	}

	// Encode with all valid codecs
	for (Codec<?> codec : codecs) {

	    // Decode the codec entry!
	    if (((Codec<T>) codec).decode(this, object)) {
		break;
	    }
	}

	return object;
    }

    private List<Object> getLazyCachedObjects() {
	if (lazyCachedObjects == null) {
	    lazyCachedObjects = new ArrayList<Object>(cachedObjectsListSize);
	}

	return lazyCachedObjects;
    }

    private <T> T assign(T object, boolean cached) {
	if (cached) {
	    getLazyCachedObjects().add(object);
	}

	return object;
    }

    private void refill(int minimalSize) throws IOException {
	// Calc limit
	limit -= pos;

	// Decrease minimal size
	minimalSize -= limit;

	// Compact the byte array
	System.arraycopy(buffer, pos, buffer, 0, limit);

	// Reset position
	pos = 0;

	// Refill the buffer
	limit += readFromStream(buffer, limit, cap - limit, minimalSize);
    }

    /*
     * (non-Javadoc)
     * @see java.io.Closeable#close()
     */
    @Override
    public void close() throws IOException {
	// Close the stream
	verifyInputStream().close();
    }

    public DefaultInput(CodecRegistry codecSystem, int bufferSize,
	    InputStream inputStream, int cachedObjectsListSize)
	    throws IOException {
	super(codecSystem, bufferSize);

	// Save
	this.cachedObjectsListSize = cachedObjectsListSize;

	// Set limit to postion!
	limit = pos;

	// Save input stream
	this.inputStream = inputStream;
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Input#readObject()
     */
    @Override
    public Object readObject() throws Exception {
	try {
	    recursiveDepth++;

	    // Read type class
	    int tc = readUnsignedByte();

	    // Check cache flag
	    boolean cached = Flag.get(tc, Protocol.CACHE_FLAG);

	    // Clear the bit!
	    tc = Flag.clear(tc, Protocol.CACHE_FLAG);

	    // Switch type class!
	    switch (tc) {
	    case Protocol.TC_CLEAR_CACHE:
		/*
		 * Very important! This indicates that the output has cleared
		 * its cache which means that we have to restart counting!
		 */
		lazyCachedObjects.clear();

		// Read the object again!
		return readObject();
	    case Protocol.TC_NULL:
		return null;
	    case Protocol.TC_CACHE_REF:
		return getLazyCachedObjects().get(readInt());
	    case Protocol.TC_BYTE:
		return readByte();
	    case Protocol.TC_BOOLEAN:
		return readBoolean();
	    case Protocol.TC_CHAR:
		return readChar();
	    case Protocol.TC_SHORT:
		return readShort();
	    case Protocol.TC_INT:
		return readInt();
	    case Protocol.TC_LONG:
		return readLong();
	    case Protocol.TC_FLOAT:
		return readFloat();
	    case Protocol.TC_DOUBLE:
		return readDouble();
	    case Protocol.TC_EMPTY_STRING:
		return readEmptyString();
	    case Protocol.TC_STRING:
		return readString(cached);
	    case Protocol.TC_ARRAY:
		return readArray(cached);
	    case Protocol.TC_PRIMITIVE_ARRAY:
		return readPrimitiveArray(cached);
	    case Protocol.TC_CLASS:
		return readClass(cached);
	    case Protocol.TC_ENUM:
		return readEnum(cached);
	    case Protocol.TC_OBJECT:
		return readOrdinaryObject(cached);
	    case Protocol.TC_SERIALIZABLE_OBJECT:
		return readSerializableObject(cached);
	    default:
		throw new IllegalStateException("Unknown type class: " + tc);
	    }
	} finally {
	    recursiveDepth--;
	}
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Input#getSource()
     */
    @Override
    public InputStream getSource() {
	return inputStream;
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Input#getSource(java.lang.Class)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T extends InputStream> T getSource(Class<T> clazz) {
	return (T) getSource();
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Input#setSource(java.io.InputStream)
     */
    @Override
    public InputStream setSource(InputStream inputStream) {
	// Get old input stream
	InputStream oldInputStream = this.inputStream;

	// Save input stream
	this.inputStream = inputStream;

	return oldInputStream;
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.BufferedResource#setMinimalBufferSize(int)
     */
    @Override
    public void setMinimalBufferSize(int bufferSize) {
	if (bufferSize < getBufferSize()) {
	    return;
	}

	// Create new buffer and set capacity
	byte[] newBuffer = new byte[bufferSize];

	// Get remaining
	int rem = limit - pos;

	// Copy content and set new limit
	System.arraycopy(buffer, pos, newBuffer, 0, rem);

	// Set the buffer
	setBuffer(newBuffer);

	// Set the limit
	limit = rem;
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Input#readByte()
     */
    @Override
    public byte readByte() throws IOException {
	if (limit - pos < 1) {
	    refill(1);
	}

	return buffer[pos++];
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Input#readBoolean()
     */
    @Override
    public boolean readBoolean() throws IOException {
	if (limit - pos < 1) {
	    refill(1);
	}

	return buffer[pos++] == 1;
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Input#readUnsignedByte()
     */
    @Override
    public int readUnsignedByte() throws IOException {
	return readByte() & 0xFF;
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Input#readUnsignedShort()
     */
    @Override
    public int readUnsignedShort() throws IOException {
	return readShort() & 0xFFFF;
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Input#readChar()
     */
    @Override
    public char readChar() throws IOException {
	if (limit - pos < 2) {
	    refill(2);
	}

	pos += 2;
	return (char) (((buffer[pos - 2]) << 8) + (buffer[pos - 1] & 0xFF));
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Input#readShort()
     */
    @Override
    public short readShort() throws IOException {
	if (limit - pos < 2) {
	    refill(2);
	}

	pos += 2;
	return (short) (((buffer[pos - 2]) << 8) + (buffer[pos - 1] & 0xFF));
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Input#readInt()
     */
    @Override
    public int readInt() throws IOException {
	if (limit - pos < 4) {
	    refill(4);
	}

	pos += 4;
	return (((buffer[pos - 4]) << 24) + ((buffer[pos - 3] & 0xFF) << 16)
		+ ((buffer[pos - 2] & 0xFF) << 8) + (buffer[pos - 1] & 0xFF));
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Input#readLong()
     */
    @Override
    public long readLong() throws IOException {
	if (limit - pos < 8) {
	    refill(8);
	}
	pos += 8;
	return ((((long) buffer[pos - 8]) << 56)
		+ ((buffer[pos - 7] & 0xFFL) << 48)
		+ ((buffer[pos - 6] & 0xFFL) << 40)
		+ ((buffer[pos - 5] & 0xFFL) << 32)
		+ ((buffer[pos - 4] & 0xFFL) << 24)
		+ ((buffer[pos - 3] & 0xFFL) << 16)
		+ ((buffer[pos - 2] & 0xFFL) << 8) + (buffer[pos - 1] & 0xFFL));
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Input#readFloat()
     */
    @Override
    public float readFloat() throws IOException {
	return Float.intBitsToFloat(readInt());
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Input#readDouble()
     */
    @Override
    public double readDouble() throws IOException {
	return Double.longBitsToDouble(readLong());
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Input#read(byte[])
     */
    @Override
    public void read(byte[] bytes) throws IOException {
	read(bytes, 0, bytes.length);
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Input#read(byte[], int, int)
     */
    @Override
    public void read(byte[] bytes, int offset, int length) throws IOException {
	// Calc copy length
	int copyLen = Math.min(length, limit - pos);

	// Check length!
	if (copyLen > 0) {

	    // Copy to bytes
	    System.arraycopy(buffer, pos, bytes, offset, copyLen);

	    // Increase
	    pos += copyLen;

	    // Decrease
	    length -= copyLen;
	}

	// Try to read the remaining bytes from the input stream
	readFromStream(bytes, offset + copyLen, length, length);
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Input#readOutputStream(java.io.OutputStream)
     */
    @Override
    public void readOutputStream(OutputStream outputStream) throws IOException {

	// Tmpvars
	int chunkSize, rem;

	for (;;) {
	    // Read chunk size!
	    chunkSize = readInt();

	    // Reached end of stream!
	    if (chunkSize == -1) {
		return;
	    }

	    while (chunkSize > 0) {
		// Refill the buffer
		if (limit - pos == 0) {
		    refill(1);
		}

		// Request buffer!
		rem = Math.min(chunkSize, limit - pos);

		// Copy to output stream
		outputStream.write(buffer, pos, rem);

		// Set position
		pos += rem;

		// Decrease rem
		chunkSize -= rem;
	    }
	}
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Input#readOutputStream(java.io.OutputStream, long)
     */
    @Override
    public void readOutputStream(OutputStream outputStream, long length)
	    throws IOException {

	// Tmp
	int rem;

	// While length > 0
	while (length > 0) {

	    // Refill the buffer
	    if (limit - pos == 0) {
		refill(1);
	    }

	    // Read buffer and write to output stream
	    outputStream.write(buffer, pos, rem = limit - pos);

	    // Increase pos
	    pos += rem;

	    // Decrease length
	    length -= rem;
	}
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Input#readObject(java.lang.Class)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T> T readObject(Class<T> clazz) throws Exception {
	return (T) readObject();
    }
}
